﻿using System;
using System.Collections.Generic;
using System.Linq;
using Google.Contacts;
using Google.GData.Client;
using Informant.Contacts;
using Informant.Engines.Contacts;
using Informant.ErrorHandling;
using Informant.Services.Models.Contacts;
using Contact = Informant.Services.Models.Contacts.Contact;
using Group = Informant.Services.Models.Contacts.Group;

namespace Informant.Engines.Google.Contacts
{
	public class GoogleContactsEngine : IContactsEngine
	{
		#region Fields

		static readonly String ApplicationName = System.Reflection.Assembly.GetExecutingAssembly().FullName;
		const String SystemGroupFlag = "System Group: ";
		// Use the ContactsRequest property to access this _contactsRequest field.
		ContactsRequest _contactsRequest = null;
		String _authToken = null;
		IErrorHandler _errorHandler = null;
		// FUTURE: Contact and Group caching needs to have a way to update if there have been changes made to the original collection.
		IEnumerable<IContact> _contactsCache = null;
		IEnumerable<IGroup> _groupsCache = null;

		#endregion

		#region Properties

		/// <summary>
		/// Gets the contacts request.
		/// </summary>
		/// <value>The contacts request.</value>
		private ContactsRequest ContactsRequest
		{
			get
			{
				if (_contactsRequest == null)
				{
					_contactsRequest = new ContactsRequest(
						new RequestSettings(
							ApplicationName));
					_contactsRequest.Service.SetAuthenticationToken(_authToken);
				}
				return _contactsRequest;
			}
		}

		#endregion

		#region Construction

		/// <summary>
		/// Initializes a new instance of the <see cref="GoogleContactsEngine"/> class.
		/// </summary>
		public GoogleContactsEngine()
		{
			_errorHandler = new ContactsErrorHandler();
		}

		#endregion

		#region IAuthenticationClient Members

		/// <summary>
		/// Gets a value indicating whether this instance is logged on.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is logged on; otherwise, <c>false</c>.
		/// </value>
		public Boolean LoggedOn
		{
			get
			{
				return !String.IsNullOrEmpty(_authToken);
			}
		}
		/// <summary>
		/// Logs on the specified user.
		/// </summary>
		/// <param name="username">The username.</param>
		/// <param name="password">The password.</param>
		/// <returns>True if successful.</returns>
		/// <exception cref="System.Security.Authentication.AuthenticationException"/>
		/// <exception cref="System.Net.WebException"/>
		public Boolean LogOn(String username, String password)
		{
			ContactsRequest contactsRequest = new ContactsRequest(
				new RequestSettings(
					ApplicationName,
					new GDataCredentials(username, password)));
			try
			{
				_authToken = contactsRequest.Service.QueryAuthenticationToken();
			}
			catch (ArgumentNullException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (GDataRequestException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (InvalidCredentialsException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (CaptchaRequiredException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (AccountDisabledException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}

			if (!LoggedOn)
			{
				throw new System.Security.Authentication.AuthenticationException(ErrorResources.AuthenticationExceptionInvalidCredentialsOrTargetServerAddress);
			}
			return LoggedOn;
		}
		/// <summary>
		/// Logs off the current user.
		/// </summary>
		/// <returns>True if successful.</returns>
		public Boolean LogOff()
		{
			_authToken = null;
			return !LoggedOn;
		}

		#endregion

		#region IContactsClient Members

		/// <summary>
		/// Retrieves all contacts.
		/// </summary>
		/// <returns></returns>
		/// <exception cref="System.Security.Authentication.AuthenticationException"/>
		/// <exception cref="System.Net.WebException"/>
		/// <exception cref="System.InvalidOperationException"/>
		public IEnumerable<IContact> RetrieveAllContacts()
		{
			IEnumerable<IContact> contacts = null;
			try
			{
				if (_contactsCache == null)
				{
					contacts = RetrieveContactsFromServer();
					_contactsCache = contacts;
				}
				else
				{
					contacts = _contactsCache;
				}
			}
			catch (ArgumentNullException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (GDataRequestException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (InvalidCredentialsException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (CaptchaRequiredException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			return contacts;
		}

		#endregion

		#region IGroupsClient Members

		/// <summary>
		/// Retrieves all groups.
		/// </summary>
		/// <returns></returns>
		/// <exception cref="System.Security.Authentication.AuthenticationException"/>
		/// <exception cref="System.Net.WebException"/>
		/// <exception cref="System.InvalidOperationException"/>
		public IEnumerable<IGroup> RetrieveAllGroups()
		{
			IEnumerable<IGroup> groups = null;
			try
			{
				if (_groupsCache == null)
				{
					groups = RetrieveGroupsFromServer();
					_groupsCache = groups;
				}
				else
				{
					groups = _groupsCache;
				}
			}
			catch (ArgumentNullException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (GDataRequestException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (InvalidCredentialsException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			catch (CaptchaRequiredException ex)
			{
				_errorHandler.MakeExceptionClientFriendly(ex);
			}
			return groups;
		}

		#endregion

		#region Private Methods

		#region IContactsClient Helpers

		/// <summary>
		/// Retrieves the contacts from server.
		/// </summary>
		/// <returns></returns>
		/// <exception cref="System.ArgumentNullException"/>
		/// <exception cref="Google.GData.Client.GDataRequestException"/>
		/// <exception cref="Google.GData.Client.InvalidCredentialsException"/>
		/// <exception cref="Google.GData.Client.CaptchaRequiredException"/>
		private IEnumerable<IContact> RetrieveContactsFromServer()
		{
			IEnumerable<IContact> contacts = null;
			if (LoggedOn)
			{
				ModifyRequesterSettings();
				// Not forcing the Entries property to iterate sometimes causes an error while iterating.
				contacts = CreateContacts(ContactsRequest.GetContacts().Entries.ToList());
			}
			return contacts;
		}
		/// <summary>
		/// Creates the contacts from Google contacts.
		/// </summary>
		/// <param name="googleContacts">The google contacts.</param>
		/// <returns></returns>
		private IEnumerable<IContact> CreateContacts(IEnumerable<global::Google.Contacts.Contact> googleContacts)
		{
			List<IContact> contacts = new List<IContact>();
			if (googleContacts != null)
			{
				foreach (var googleContact in googleContacts)
				{
					contacts.Add(CreateContact(googleContact));
				}
			}
			return contacts;
		}
		/// <summary>
		/// Creates the contact from a Google contact.
		/// </summary>
		/// <param name="googleContact">The google contact.</param>
		/// <returns></returns>
		private IContact CreateContact(global::Google.Contacts.Contact googleContact)
		{
			Contact contact = new Contact();

			contact.Id = googleContact.Id;

			String[] firstAndLastName = GetFirstAndLastNameFromGoogleFormattedName(googleContact.Title);
			contact.FirstName = firstAndLastName[0];
			contact.LastName = firstAndLastName[1];

			contact.Addresses = CreateAddresses(googleContact.PostalAddresses.ToList());
			contact.Phones = CreatePhones(googleContact.Phonenumbers.ToList());
			contact.Emails = CreateEmails(googleContact.Emails.ToList());
			contact.Groups = CreateGroups(googleContact.GroupMembership.ToList());
			contact.Organizations = CreateOrganizations(googleContact.Organizations.ToList());

			return contact;
		}
		/// <summary>
		/// Gets the first and last name of the contact from a Google Contact formatted name.
		///	The returned String[] contains the first name at index 0 and the last name at index 1.
		/// </summary>
		/// <param name="googleContactFormatedName">Name of the google contact formated.</param>
		/// <exception cref="System.ArgumentNullException"/>
		private String[] GetFirstAndLastNameFromGoogleFormattedName(String googleContactFormattedName)
		{
			// This method could use some refactoring...
			String[] firstAndLastName = new String[2];
			String[] splittingArgs = null;
			Int32 firstNameIndex = 0;
			Int32 lastNameIndex = 0;
			if (!String.IsNullOrEmpty(googleContactFormattedName))
			{
				if (googleContactFormattedName.Contains(','))
				{
					splittingArgs = new String[] { "," };
					firstNameIndex = 1;
					lastNameIndex = 0;
				}
				else
				{
					splittingArgs = new String[] { " " };
					firstNameIndex = 0;
					lastNameIndex = 1;
				}
				String[] nameArray = googleContactFormattedName.Split(splittingArgs, StringSplitOptions.RemoveEmptyEntries);
				if (nameArray.Count() == 2)
				{
					firstAndLastName[0] = nameArray[firstNameIndex].Trim();
					firstAndLastName[1] = nameArray[lastNameIndex].Trim();
				}
				else
				{
					firstAndLastName[0] = nameArray[firstNameIndex].Trim();
					firstAndLastName[1] = googleContactFormattedName.Trim();
				}
			}
			return firstAndLastName;
		}
		/// <summary>
		/// Creates the addresses from an enumerable of Google addresses.
		/// </summary>
		/// <param name="googleAddresses">The google addresses.</param>
		/// <returns></returns>
		private IEnumerable<IAddress> CreateAddresses(IEnumerable<global::Google.GData.Extensions.PostalAddress> googleAddresses)
		{
			List<IAddress> addresses = new List<IAddress>();

			if (googleAddresses != null)
			{
				foreach (var googleAddress in googleAddresses)
				{
					addresses.Add(CreateAddress(googleAddress));
				}
			}

			return addresses;
		}
		/// <summary>
		/// Creates the address from a Google address.
		/// </summary>
		/// <param name="googleAddress">The google address.</param>
		/// <returns></returns>
		private IAddress CreateAddress(global::Google.GData.Extensions.PostalAddress googleAddress)
		{
			Address address = new Address();

			if (googleAddress.Rel.ToLower().EndsWith(InformationKind.Home.ToString().ToLower()))
			{
				address.Kind = InformationKind.Home;
			}
			else if (googleAddress.Rel.ToLower().EndsWith(InformationKind.Work.ToString().ToLower()))
			{
				address.Kind = InformationKind.Work;
			}
			else if (googleAddress.Rel.ToLower().EndsWith(InformationKind.Other.ToString().ToLower()))
			{
				address.Kind = InformationKind.Other;
			}
			else
			{
				address.Kind = InformationKind.Other;
			}

			address.RawAddress = googleAddress.Value;

			return address;
		}
		/// <summary>
		/// Creates the emails from Google emails.
		/// </summary>
		/// <param name="googleEmails">The google emails.</param>
		/// <returns></returns>
		private IEnumerable<IEmail> CreateEmails(IEnumerable<global::Google.GData.Extensions.EMail> googleEmails)
		{
			List<IEmail> emails = new List<IEmail>();

			if (googleEmails != null)
			{
				foreach (var googleEmail in googleEmails)
				{
					emails.Add(CreateEmail(googleEmail));
				}
			}

			return emails;
		}
		/// <summary>
		/// Creates the email from a Google email.
		/// </summary>
		/// <param name="googleEmail">The google email.</param>
		/// <returns></returns>
		private IEmail CreateEmail(global::Google.GData.Extensions.EMail googleEmail)
		{
			Email email = new Email();

			email.Kind = ConvertToInformationKind(googleEmail.Rel);
			email.Address = googleEmail.Address;

			return email;
		}
		/// <summary>
		/// Creates the phones from an enumerable of Google phones.
		/// </summary>
		/// <param name="googlePhones">The google phones.</param>
		/// <returns></returns>
		private IEnumerable<IPhone> CreatePhones(IEnumerable<global::Google.GData.Extensions.PhoneNumber> googlePhones)
		{
			List<IPhone> phones = new List<IPhone>();

			if (googlePhones != null)
			{
				foreach (var googlePhone in googlePhones)
				{
					phones.Add(CreatePhone(googlePhone));
				}
			}

			return phones;
		}
		/// <summary>
		/// Creates the phone from a Google phone.
		/// </summary>
		/// <param name="phone">The phone.</param>
		/// <returns></returns>
		private IPhone CreatePhone(global::Google.GData.Extensions.PhoneNumber googlePhone)
		{
			Phone phone = new Phone();

			phone.Kind = ConvertToInformationKind(googlePhone.Rel);

			String[] phoneParts = RemovePhoneNumberFormatting(googlePhone.Value).Split('x');
			if (phoneParts.Count() > 0)
			{
				phone.Number = phoneParts[0];
				if (phoneParts.Count() == 2)
				{
					phone.Extension = phoneParts[1];
				}
			}

			return phone;
		}
		/// <summary>
		/// Removes the phone number formatting.
		/// Removes {Whitespace, (, ), -, +, ., /, \, leading 1 }
		/// Replaces {ext with x}
		/// </summary>
		/// <param name="phoneNumber">The phone number.</param>
		/// <returns></returns>
		private String RemovePhoneNumberFormatting(String phoneNumber)
		{
			String deformattedNumber = phoneNumber.Replace(" ", string.Empty);
			deformattedNumber = deformattedNumber.Replace("(", string.Empty);
			deformattedNumber = deformattedNumber.Replace(")", string.Empty);
			deformattedNumber = deformattedNumber.Replace("-", string.Empty);
			deformattedNumber = deformattedNumber.Replace("+", string.Empty);
			deformattedNumber = deformattedNumber.Replace(".", string.Empty);
			deformattedNumber = deformattedNumber.Replace("/", string.Empty);
			deformattedNumber = deformattedNumber.Replace("\\", string.Empty);
			deformattedNumber = deformattedNumber.Replace("ext", "x");

			if (deformattedNumber.StartsWith("1"))
			{
				deformattedNumber = deformattedNumber.Substring(1);
			}
			return deformattedNumber;
		}
		/// <summary>
		/// Creates the organizations from Google organizations.
		/// </summary>
		/// <param name="googleOrganizations">The google organizations.</param>
		/// <returns></returns>
		private IEnumerable<IOrganization> CreateOrganizations(IEnumerable<global::Google.GData.Extensions.Organization> googleOrganizations)
		{
			List<IOrganization> organizations = new List<IOrganization>();
			if (googleOrganizations != null)
			{
				foreach (var googleOrganization in googleOrganizations)
				{
					organizations.Add(CreateOrganization(googleOrganization));
				}
			}
			return organizations;
		}
		/// <summary>
		/// Creates the organization from a Google organization.
		/// </summary>
		/// <param name="googleOrganization">The google organization.</param>
		/// <returns></returns>
		private IOrganization CreateOrganization(global::Google.GData.Extensions.Organization googleOrganization)
		{
			Organization organization = new Organization();

			organization.Name = googleOrganization.Name;
			organization.Title = googleOrganization.Title;
			organization.Kind = ConvertToInformationKind(googleOrganization.Rel);

			return organization;
		}

		#endregion

		#region IGroupsClient Helpers

		/// <summary>
		/// Retrieves the groups from server.
		/// </summary>
		/// <returns></returns>
		/// <exception cref="System.ArgumentNullException"/>
		/// <exception cref="Google.GData.Client.GDataRequestException"/>
		/// <exception cref="Google.GData.Client.InvalidCredentialsException"/>
		/// <exception cref="Google.GData.Client.CaptchaRequiredException"/>
		private IEnumerable<IGroup> RetrieveGroupsFromServer()
		{
			IEnumerable<IGroup> groups = null;
			if (LoggedOn)
			{
				ModifyRequesterSettings();
				// Not forcing the Entries property to iterate sometimes causes an error while iterating.
				groups = CreateGroups(ContactsRequest.GetGroups().Entries.ToList());
			}
			return groups;
		}
		/// <summary>
		/// Creates the groups from Google group memberships.
		/// </summary>
		/// <param name="googleGroupMemberships">The google group memberships.</param>
		/// <returns></returns>
		private IEnumerable<IGroup> CreateGroups(IEnumerable<global::Google.GData.Contacts.GroupMembership> googleGroupMemberships)
		{
			List<IGroup> groupsInMembership = new List<IGroup>();

			if (googleGroupMemberships != null)
			{
				foreach (var googleGroupMembership in googleGroupMemberships)
				{
					foreach (var group in RetrieveAllGroups())
					{
						if (googleGroupMembership.HRef == group.Id)
						{
							groupsInMembership.Add(group);
							break;
						}
					}
				}
			}

			return groupsInMembership;
		}
		/// <summary>
		/// Creates the groups from Google groups.
		/// </summary>
		/// <param name="googleGroups">The google groups.</param>
		/// <returns></returns>
		private IEnumerable<IGroup> CreateGroups(IEnumerable<global::Google.Contacts.Group> googleGroups)
		{
			List<IGroup> groups = new List<IGroup>();
			if (googleGroups != null)
			{
				foreach (var googleGroup in googleGroups)
				{
					groups.Add(CreateGroup(googleGroup));
				}
			}
			return groups;
		}
		/// <summary>
		/// Creates the group from a Google group.
		/// </summary>
		/// <param name="googleGroup">The google group.</param>
		/// <returns></returns>
		private IGroup CreateGroup(global::Google.Contacts.Group googleGroup)
		{
			Group group = new Group();
			group.Id = googleGroup.Id;
			group.Name = googleGroup.Title.StartsWith(SystemGroupFlag) ? googleGroup.Title.Substring(SystemGroupFlag.Length) : googleGroup.Title;

			return group;
		}

		#endregion

		/// <summary>
		/// Converts the kind of to information from a Google relationship String.
		/// </summary>
		/// <param name="googleRelationship">The google relationship.</param>
		/// <returns></returns>
		private InformationKind ConvertToInformationKind(String googleRelationship)
		{
			InformationKind kind;
			if (String.IsNullOrEmpty(googleRelationship))
			{
				kind = InformationKind.Other;
			}
			else if (googleRelationship.ToLower().EndsWith(InformationKind.Mobile.ToString().ToLower()))
			{
				kind = InformationKind.Mobile;
			}
			else if (googleRelationship.ToLower().EndsWith(InformationKind.Home.ToString().ToLower()))
			{
				kind = InformationKind.Home;
			}
			else if (googleRelationship.ToLower().EndsWith(InformationKind.Home_Fax.ToString().ToLower()))
			{
				kind = InformationKind.Home_Fax;
			}
			else if (googleRelationship.ToLower().EndsWith(InformationKind.Work.ToString().ToLower()))
			{
				kind = InformationKind.Work;
			}
			else if (googleRelationship.ToLower().EndsWith(InformationKind.Work_Fax.ToString().ToLower()))
			{
				kind = InformationKind.Work_Fax;
			}
			else if (googleRelationship.ToLower().EndsWith(InformationKind.Pager.ToString().ToLower()))
			{
				kind = InformationKind.Pager;
			}
			else if (googleRelationship.ToLower().EndsWith(InformationKind.Other.ToString().ToLower()))
			{
				kind = InformationKind.Other;
			}
			else
			{
				kind = InformationKind.Other;
			}
			return kind;
		}
		/// <summary>
		/// Resets the requester settings to a default state.
		/// The default is (autoPaging = true, pageSize = -1, maximum = -1).
		/// </summary>
		private void ModifyRequesterSettings()
		{
			ContactsRequest.Settings.AutoPaging = true;
			ContactsRequest.Settings.PageSize = -1;
			ContactsRequest.Settings.Maximum = -1;
		}
		/// <summary>
		/// Modifies the requester settings.
		/// The maximum value must be greater than or equal to pageSize,
		///	if not then pageSize is used as the maximum value.
		/// </summary>
		/// <param name="autoPaging">if set to <c>true</c> [auto paging].</param>
		/// <param name="pageSize">Size of the page.</param>
		/// <param name="maximum">The maximum.</param>
		private void ModifyRequesterSettings(Boolean autoPaging, Int32 pageSize, Int32 maximum)
		{
			ContactsRequest.Settings.AutoPaging = autoPaging;
			ContactsRequest.Settings.PageSize = pageSize;
			ContactsRequest.Settings.Maximum = maximum > pageSize ? maximum : pageSize;
		}

		#endregion
	}
}
